55 绘制曲线图
本章定位(复习与补充训练层):本章对应正课第18章《Matplotlib库基础》(章节 18,曲线绘制与同一按揭任务的另一种口径)以及第20章《常用图形绘制》(章节 20)中”时间序列用折线图”的选型要点,并与第49章《数据框可视化》(章节 50)互为补充训练(本章为分项版),用于复习、补缺与额外练习。建议先不看讲解,直接尝试下方平台任务,再对照解析补弱项。本章不属于必修主线。先做本章『动手与思考』第 1 题与平台任务自测,通过即可跳过本章。
55.1 引言曲线图的力量
曲线图(Line Plot)是最基础也是最重要的数据可视化形式之一,在金融领域帮助我们观察趋势、发现季节性与周期模式、比较不同资产的表现并监控关键指标的变化。
55.2 本章学习目标
通过本章复习,你将能够:
- 不查资料复述
plt.plot的常用参数(颜色-线型格式串、lw、label)以及xlabel/ylabel/title/legend/grid的设置顺序,并据此复原平台任务的绘图骨架 - 解释等额本息与等额本金两种还款规则下”月供、本金、利息”三条曲线的形态差异及其成因,并说明
pmt、ppmt、ipmt的符号约定 - 根据数据特点在单轴多线、双Y轴(
twinx)、对数坐标(semilogy)、填充区间(fill_between)与堆叠面积图(stackplot)中作出选择,并说出各自的适用场景与误导风险 - 用
rolling计算移动平均线,标注金叉与死叉,并解释移动平均信号的滞后性
先修:章节 18、章节 6(第18章的绘图基础与第6章的数组运算,本章平台任务中的 arange、linspace、ones_like 均由 NumPy 提供)。
55.3 曲线图的数学基础
曲线图本质上是函数的可视化:
\[ y = f(x) \]
在金融时间序列中:
- 自变量 \(x\): 时间 \(t\)
- 因变量 \(y\): 价格、收益率、成交量等
连续与离散:
- 理论上,价格是离散的(仅在交易时刻有定义)
- 但为了可视化,我们用线段连接相邻点,形成连续曲线
- 这种线性插值假设价格在交易间隔内平滑变化
55.4 基础曲线图单条线
任务要求(归纳自块内任务注释与代码):按给定按揭参数(本金 600 万元、年利率 4.9%、期限 30 年)用 pmt 计算等额本息月供,取负并保留 2 位小数后用 print 输出;生成 360 期的期数数组,用 ppmt/ipmt 逐期计算本金与利息部分;在同一坐标轴绘制三条曲线——每月偿还金额、每月偿还本金、每月偿还利息(均取负翻正),设置 X/Y 轴标签、图例与网格,将图形保存为 1.png。请将代码原样输入教学平台(注释除外),判定以平台为准。
平台任务1(平台原始代码)
以下代码与教学平台任务要求完全一致:
# 注:numpy_financial包已安装于本地peter环境(1.0.0),平台亦已内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import numpy as np # 导入NumPy数值计算库
import pandas as pd # 导入Pandas数据分析库
import matplotlib.pyplot as plt #导入matplotlab的子模块pyplot
import numpy_financial as nmf # 导入NumPy库
plt.rcParams["font.sans-serif"] = ["SimHei"] # 设置Matplotlib全局参数
r = 0.049 #贷款的年利率
n = 30 #贷款的期限(年)
principle = 6e6 #贷款的本金
pay_month = nmf.pmt(rate=r/12,nper=n*12,pv=principle,fv=0,when="end") # 计算等额本息月供金额
print("在等额本息规则下丁先生每月偿还的金额",-round(pay_month,2)) # 输出在等额本息规则下丁先生每月偿还的金额
t = np.arange(n*12)+1 #生成一个包含每次还款期限长度的数组
principle_pay_month = nmf.ppmt(rate=r/12,per=t,nper=n*12,pv=principle,fv=0,when="end") #计算每月偿还的本金额
interest_pay_month = nmf.ipmt(rate=r/12,per=t,nper=n*12,pv=principle,fv=0,when="end") #计算每月偿还的利息额
pay_month_array = pay_month*np.ones_like(principle_pay_month) #创建一个每月偿还的数组
plt.figure(figsize=(9,6)) # 创建图形画布
plt.plot(t,-pay_month_array,"r-",label=u"每月偿还金额",lw=2.5) # 绘制折线图
plt.plot(t,-principle_pay_month,"m--",label=u"每月偿还本金金额",lw=2.5) # 绘制折线图
plt.plot(t,-interest_pay_month,"b--",label =u"每月产股韩利息金额",lw=2.5) # 绘制折线图
plt.xticks(fontsize=14) # 设置X轴刻度标签
plt.xlabel(u"逐次偿还的期限(月)",fontsize=14) # 设置X轴标签
plt.yticks(fontsize=13) # 设置Y轴刻度标签
plt.ylabel(u"金额",fontsize=14) # 设置Y轴标签
plt.title(u"等额本息还款规则下每月偿还的金额以及本金额与利息额") # 设置图表标题
plt.legend(loc=0,fontsize=13) # 添加图例
plt.grid() # 显示网格线
plt.savefig("1.png") # 保存图形至文件预期输出(本机实际运行结果;以平台运行结果为准):
在等额本息规则下丁先生每月偿还的金额 31843.6
此外无其他控制台输出,并生成文件 1.png:一条红色的”每月偿还金额”水平直线(恒为 31,843.6 元)、一条逐期上升的洋红虚线(每月本金,自首月 7,343.60 元升至末期 31,714.10 元)、一条逐期下降的蓝色虚线(每月利息,自首月 24,500.00 元降至末期 129.50 元),任意一期两条虚线之和恒等于月供 31,843.60 元。本地环境缺少 SimHei 字体时图上中文显示为方框并伴随 findfont 警告,属环境问题而非代码错误。
注:上图第3条曲线的图例字符串 u"每月产股韩利息金额" 是平台原始题面中的乱码,正确含义是”每月偿还利息金额”;因平台按原始代码判定,请原样输入,本地自主练习时写作 u"每月偿还利息金额" 即可。
另外,第18章的同一按揭贷款任务用 ppmt(rate=r/12,per=n*12,...) 计算每月偿还金额(其口径是末期的本金部分),而本章用 pmt 计算等额本息月供,两章口径不同、各自正确。
任务要求(归纳自块内任务注释与代码):用 linspace 生成 2%~8% 共 100 档贷款利率,逐档计算 30 年期、600 万元本金的等额本息月供;绘制月供随利率上升的红色曲线,并用蓝色圆点标出实际利率 4.9% 对应的月供(图例注明”贷款利率4.9%的每月偿还金额”),设置轴标签、图例与网格,保存为 2.png。请将代码原样输入教学平台(注释除外),判定以平台为准。
平台任务2(平台原始代码)
以下代码与教学平台任务要求完全一致:
# 注:numpy_financial包已安装于本地peter环境(1.0.0),平台亦已内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import numpy as np # 导入NumPy数值计算库
import matplotlib.pyplot as plt #导入matplotlab的子模块pyplot
import numpy_financial as nmf # 导入NumPy库
plt.rcParams["font.sans-serif"] = ["SimHei"] # 设置Matplotlib全局参数
r = 0.049 #贷款的年利率
n = 30 #贷款的期限(年)
principle = 6e6 #贷款的本金
r_list = np.linspace(0.02,0.08,100) #生成贷款利率的一个数组
pay_month = nmf.pmt(rate=r/12,nper=n*12,pv=principle,fv=0,when="end") # 计算等额本息月供金额
pay_month_list = nmf.pmt(rate=r_list/12,nper=n*12,pv=principle,fv=0,when="end") #计算不同贷款利率条件下的每月偿还本息之和
plt.figure(figsize=(9,6)) # 创建图形画布
plt.plot(r_list,-pay_month_list,"r-",label=u"每月偿还金额",lw=2.5) # 绘制折线图
plt.plot(r,-pay_month,"bo",label=u"贷款利率4.9%的每月偿还金额") # 绘制折线图
plt.xticks(fontsize=14) # 设置X轴刻度标签
plt.xlabel(u"贷款利率",fontsize=14) # 设置X轴标签
plt.yticks(fontsize=14) # 设置Y轴刻度标签
plt.ylabel(u"金额",fontsize=14,rotation=90) # 设置Y轴标签
plt.legend(loc=0,fontsize=13) # 添加图例
plt.grid() # 显示网格线
plt.savefig("2.png") # 保存图形至文件预期输出(本机实际运行结果;以平台运行结果为准):本块无控制台输出,生成文件 2.png——一条随利率上升而上升的红色月供曲线(2%~8% 共 100 个点)与 4.9% 处的蓝色圆点标记;圆点的纵坐标与任务一输出的 31843.6 一致,可互为校验。
任务要求(归纳自块内任务注释与代码):按等额本金规则计算 360 期还款——每月偿还本金固定为 600 万/360,每月利息按剩余本金以月利率循环重算;在同一坐标轴绘制三条曲线——每月还款总额、每月偿还本金(黄色虚线,水平)、每月偿还利息(青色虚线,递减),设置轴标签、标题、图例与网格,保存为 3.png。请将代码原样输入教学平台(注释除外),判定以平台为准。
平台任务3(平台原始代码)
以下代码与教学平台任务要求完全一致:
# 注:numpy_financial包已安装于本地peter环境(1.0.0),平台亦已内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import numpy as np # 导入NumPy数值计算库
import matplotlib.pyplot as plt #导入matplotlab的子模块pyplot
import numpy_financial as nmf # 导入NumPy库
plt.rcParams["font.sans-serif"] = ["SimHei"] # 设置Matplotlib全局参数
r = 0.049 #贷款的年利率
n = 30 #贷款的期限(年)
principle = 6e6 #贷款的本金
t = np.arange(n*12)+1 #生成一个包含每次还款期限长度的数组
prin_month = principle/(n*12) #计算等额本金还款规则下的每月本金还款额
prin_month_array = np.ones(n*12)*prin_month #生成一个每月本金还款额的数组
int_month_array = np.zeros_like(prin_month_array) #生成存放每月利息还款额的初始数组
for i in np.arange(n*12): # 遍历np.arange(n*12)中的每个i
int_month_array[i] = (principle-i*prin_month)*r/12 #计算逐月支付的利息额
pay_total_month = prin_month_array+int_month_array #j计算等额本金还款规则下的每月还款总额
plt.figure(figsize=(9,6)) # 创建图形画布
plt.plot(t,pay_total_month,"m-",label=u"每月偿还金额",lw=2.5) # 绘制折线图
plt.plot(t,prin_month_array,"y--",label=u"每月偿还本金额",lw=2.5) # 绘制折线图
plt.plot(t,int_month_array,"c--",label =u"每月偿还利息额",lw=2.5) # 绘制折线图
plt.xticks(fontsize=14) # 设置X轴刻度标签
plt.xlabel(u"逐次偿还的期限(月)",fontsize=14) # 设置X轴标签
plt.yticks(fontsize=14) # 设置Y轴刻度标签
plt.ylabel(u"金额",fontsize=14,rotation=90) # 设置Y轴标签
plt.title(u"等额本金还款规则下每月偿还的金额以及本金额与利息额",fontsize=14) # 设置图表标题
plt.legend(loc=0,fontsize=13) # 添加图例
plt.grid() # 显示网格线
plt.savefig("3.png") # 保存图形至文件预期输出(本机实际运行结果;以平台运行结果为准):本块无控制台输出,生成文件 3.png——洋红色的”每月偿还金额”曲线自首月 41,166.67 元逐期线性递减至末期 16,734.72 元,黄色虚线的每月本金恒为 16,666.67 元(600 万/360),青色虚线的每月利息自首月 24,500.00 元递减至末期 68.06 元;首月月供 41,166.67 元高于等额本息的 31,843.6 元,末期则低于它,这正是等额本金”月供逐期递减”的形态。
# =============================================================================
# 题目:基础曲线图——贵州茅台股价走势
# =============================================================================
# 本任务演示如何使用Matplotlib绘制基础曲线图,展示单条时间序列数据
# 应用场景:可视化单个资产的股价走势
# ==================== 导入必要的库 ====================
import matplotlib.pyplot as plt # Matplotlib的pyplot接口,提供类似MATLAB的绘图API
import pandas as pd # Pandas数据分析库,用于数据处理
import numpy as np # NumPy数值计算库
# ==================== 设置中文字体支持 ====================
# Matplotlib默认不支持中文显示,需要手动设置中文字体
# plt.rcParams是一个全局配置字典,用于控制绘图的各种属性
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC', 'SimHei'] # 规范字体在前,SimHei 仅作后备 # 设置默认字体为黑体(SimHei),确保中文正常显示
plt.rcParams['axes.unicode_minus'] = False # 解决负号'-'显示为方块的问题
# 这两行设置确保图表中的中文文字和负号都能正确显示
# ==================== 创建示例数据 ====================
# 场景:模拟贵州茅台30个交易日的股价数据
# pd.date_range()生成日期序列
# '2024-01-01':起始日期
# periods=30:生成30个日期(30个交易日)
dates = pd.date_range('2024-01-01', periods=30)
# 使用列表推导式生成模拟股价数据
# 基础价格1850元
# i * 2:每天上涨2元(模拟上涨趋势)
# np.random.randn() * 10:添加随机波动,标准差为10元
np.random.seed(42) # 设置随机种子,确保每次运行结果相同
prices = [1850 + i * 2 + np.random.randn() * 10 for i in range(30)]
# prices是一个包含30个价格的列表,模拟真实股价的随机波动
# 创建DataFrame便于数据管理
# DataFrame是Pandas的核心数据结构,类似Excel表格
df = pd.DataFrame({
'日期': dates, # 日期列,作为X轴
'收盘价': prices # 收盘价列,作为Y轴
})
# ==================== 绘制基础曲线图 ====================
# plt.figure()创建一个新的图形窗口
# figsize=(12, 6):图形大小为12英寸宽×6英寸高
# 这个比例适合展示时间序列数据,宽度较大以容纳更多时间点
plt.figure(figsize=(12, 6))
# plt.plot()绘制折线图(曲线图)
# df['日期']:X轴数据(时间序列)
# df['收盘价']:Y轴数据(价格)
# linewidth=2:线条宽度为2,使折线更清晰可见
# color='#2E86AB':线条颜色,使用十六进制颜色代码(深蓝色)
plt.plot(df['日期'], df['收盘价'], linewidth=2, color='#2E86AB')
# ==================== 设置图表装饰 ====================
# plt.title()设置图表标题
# fontsize=16:标题字号为16(较大,突出显示)
# fontweight='bold':标题文字加粗
plt.title('贵州茅台股价走势', fontsize=16, fontweight='bold')
# plt.xlabel()设置X轴标签
# fontsize=12:标签字号为12
plt.xlabel('日期', fontsize=12)
# plt.ylabel()设置Y轴标签
plt.ylabel('收盘价(元)', fontsize=12)
# plt.grid()添加网格线,帮助读取数值
# True:显示网格线
# alpha=0.3:网格线透明度为0.3(使网格不显眼,不干扰数据)
plt.grid(True, alpha=0.3)
# plt.xticks()设置X轴刻度标签的显示方式
# rotation=45:刻度标签旋转45度(防止日期文字重叠)
plt.xticks(rotation=45)
# plt.tight_layout()自动调整布局,防止元素重叠(如标题被截断)
plt.tight_layout()
# plt.show()将图形显示出来
plt.show()
# ==================== 输出数据统计信息 ====================
print('数据统计:')
# df['收盘价'].describe()计算收盘价的描述性统计量
# 包括:计数、均值、标准差、最小值、四分位数、最大值
print(df['收盘价'].describe())代码解析:
中文字体设置:
plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = FalseSimHei:黑体,确保中文正常显示unicode_minus:解决负号显示问题
plot函数参数:
linewidth:线宽,数值越大线越粗color:颜色,可以用十六进制、RGB、英文名称linestyle:线型,'-'(实线)、'--'(虚线)、':'(点线)
图表装饰:
title:标题xlabel/ylabel:轴标签grid:网格线tight_layout:自动调整布局,避免元素重叠
55.5 多条曲线对比
# =============================================================================
# 题目:多条曲线对比——不同股票价格走势
# =============================================================================
# 本任务演示如何在同一图表中绘制多条曲线,对比不同资产的表现
# ==================== 创建多只股票数据 ====================
# 场景:模拟三只股票30个交易日的价格数据
# 使用之前生成的dates作为时间序列
np.random.seed(42) # 设置随机种子,确保每次运行结果相同
stocks_data = pd.DataFrame({
'日期': dates,
# 贵州茅台:高价股(1850元起),每天上涨1.5元,标准差8元
'贵州茅台': [1850 + i * 1.5 + np.random.randn() * 8 for i in range(30)],
# 五粮液:中价股(220元起),每天上涨0.8元,标准差5元
'五粮液': [220 + i * 0.8 + np.random.randn() * 5 for i in range(30)],
# 招商银行:低价股(45元起),每天上涨0.3元,标准差2元
'招商银行': [45 + i * 0.3 + np.random.randn() * 2 for i in range(30)]
})
# ==================== 绘制多条曲线 ====================
plt.figure(figsize=(12, 6))
# 绘制第一条曲线:贵州茅台(红色)
# label参数设置图例标签,将在图例中显示
plt.plot(stocks_data['日期'], stocks_data['贵州茅台'],
label='贵州茅台', linewidth=2, color='#E3120B')
# 绘制第二条曲线:五粮液(青色)
plt.plot(stocks_data['日期'], stocks_data['五粮液'],
label='五粮液', linewidth=2, color='#008080')
# 绘制第三条曲线:招商银行(深灰色)
plt.plot(stocks_data['日期'], stocks_data['招商银行'],
label='招商银行', linewidth=2, color='#2C3E50')
# ==================== 设置图表装饰 ====================
plt.title('不同股票价格走势对比', fontsize=16, fontweight='bold')
plt.xlabel('日期', fontsize=12)
plt.ylabel('收盘价(元)', fontsize=12)
# plt.legend()显示图例
# loc='best':自动选择最佳位置(避免遮挡数据)
# fontsize=11:图例字号为11
plt.legend(loc='best', fontsize=11)
plt.grid(True, alpha=0.3)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# ==================== 输出各股票统计信息 ====================
print('各股票统计信息:')
# describe()对每只股票分别计算统计量
print(stocks_data.describe())视觉层次设计:
当绘制多条曲线时,需要考虑:
- 颜色选择:使用对比度高、易于区分的颜色
- 线宽:主要数据用较粗的线,次要数据用较细的线
- 图例位置:
loc='best'自动选择最佳位置 - Y轴范围:如果数据量级差异大,考虑用双Y轴或归一化
55.6 双Y轴图
# =============================================================================
# 题目:双Y轴图——价格与成交量
# =============================================================================
# 本任务演示如何创建双Y轴图表,同时展示量价关系
# 应用场景:分析价格与成交量、收益率与波动率等不同量级的数据
# ==================== 创建价格和成交量数据 ====================
np.random.seed(42) # 设置随机种子,确保每次运行结果相同
df_dual = pd.DataFrame({
'日期': dates,
# 收盘价:45元起,每天上涨0.5元,标准差3元
'收盘价': [45 + i * 0.5 + np.random.randn() * 3 for i in range(30)],
# 成交量:5000手起,每天增加100手,标准差500手
# 成交量与价格的量级不同(价格是几十,成交量是几千),需要双Y轴
'成交量': [5000 + i * 100 + np.random.randn() * 500 for i in range(30)]
})
# ==================== 创建图表和轴对象 ====================
# plt.subplots()创建图形和轴对象
# fig:图形对象,包含整个图形
# ax1:左Y轴对象(第一个轴)
# figsize=(12, 6):图形大小
fig, ax1 = plt.subplots(figsize=(12, 6))
# ==================== 绘制左Y轴(价格) ====================
color1 = '#E3120B' # 红色,用于价格线
# ax1.set_xlabel()设置X轴标签
ax1.set_xlabel('日期', fontsize=12)
# ax1.set_ylabel()设置左Y轴标签
# color=color1:标签颜色与线条颜色一致,便于识别
ax1.set_ylabel('收盘价(元)', color=color1, fontsize=12)
# ax1.plot()在左Y轴上绘制数据
# color=color1:线条颜色
# linewidth=2:线宽
# label='收盘价':图例标签
line1 = ax1.plot(df_dual['日期'], df_dual['收盘价'],
color=color1, linewidth=2, label='收盘价')
# ax1.tick_params()设置刻度标签样式
# axis='y':只设置Y轴刻度
# labelcolor=color1:Y轴刻度标签颜色与线条颜色一致
ax1.tick_params(axis='y', labelcolor=color1)
# ax1.grid()只在左Y轴添加网格线
ax1.grid(True, alpha=0.3)
# ==================== 创建共享X轴的右Y轴 ====================
# ax1.twinx()创建一个共享X轴的新Y轴(右Y轴)
# 这样两个Y轴对应同一个X轴(时间轴)
ax2 = ax1.twinx()
# ==================== 绘制右Y轴(成交量) ====================
color2 = '#008080' # 青色,用于成交量线
# ax2.set_ylabel()设置右Y轴标签
ax2.set_ylabel('成交量(手)', color=color2, fontsize=12)
# ax2.plot()在右Y轴上绘制数据
# linestyle='--':使用虚线,与实线的价格线区分
line2 = ax2.plot(df_dual['日期'], df_dual['成交量'],
color=color2, linewidth=2, linestyle='--', label='成交量')
# ax2.tick_params()设置右Y轴刻度标签样式
ax2.tick_params(axis='y', labelcolor=color2)
# ==================== 合并图例 ====================
# 因为ax1和ax2各有图例,需要合并成一个统一的图例
# lines = line1 + line2:合并两条线
lines = line1 + line2
# [l.get_label() for l in lines]:提取每条线的标签
labels = [l.get_label() for l in lines]
# ax1.legend()在左Y轴上显示合并后的图例
# lines:线条对象列表
# labels:标签列表
ax1.legend(lines, labels, loc='best', fontsize=11)
# ==================== 设置标题并显示 ====================
plt.title('价格与成交量走势', fontsize=16, fontweight='bold')
plt.tight_layout()
plt.show()双Y轴的使用场景:
- 量价关系:价格与成交量
- 收益率与波动率:收益与风险
- 价格与指标:价格与移动平均线、RSI等技术指标
- 不同量级数据:股价(元)与市值(万亿元)
注意事项:
- 双Y轴可能误导读者,需谨慎使用
- 明确标注哪个轴对应哪条线
- 考虑用子图(subplots)替代,更清晰
55.7 填充区域图
# =============================================================================
# 题目:填充区域图——价格区间
# =============================================================================
# 本任务演示如何绘制填充区域图,展示预测区间或置信区间
# 应用场景:布林带、价格预测区间、期权盈亏区间
# ==================== 创建带上下限的数据 ====================
np.random.seed(42) # 设置随机种子,确保每次运行结果相同
df_area = pd.DataFrame({
'日期': dates,
# 价格:100元起,每天上涨1元,标准差3元
'价格': [100 + i + np.random.randn() * 3 for i in range(30)],
# 上限:105元起,每天上涨1元,标准差2元(价格上方5元)
'上限': [105 + i + np.random.randn() * 2 for i in range(30)],
# 下限:95元起,每天上涨1元,标准差2元(价格下方5元)
'下限': [95 + i + np.random.randn() * 2 for i in range(30)]
})
# ==================== 绘制填充区域图 ====================
plt.figure(figsize=(12, 6))
# 绘制实际价格曲线(深灰色)
plt.plot(df_area['日期'], df_area['价格'],
label='实际价格', linewidth=2, color='#2C3E50')
# plt.fill_between()填充两条曲线之间的区域
# df_area['日期']:X轴数据
# df_area['下限']:区域下边界
# df_area['上限']:区域上边界
# alpha=0.3:透明度0.3(使填充区域不遮挡网格线)
# color='#008080':填充颜色(青色)
# label='预测区间':图例标签
plt.fill_between(df_area['日期'], df_area['下限'], df_area['上限'],
alpha=0.3, color='#008080', label='预测区间')
# ==================== 设置图表装饰 ====================
plt.title('价格走势与预测区间', fontsize=16, fontweight='bold')
plt.xlabel('日期', fontsize=12)
plt.ylabel('价格(元)', fontsize=12)
plt.legend(loc='best', fontsize=11)
plt.grid(True, alpha=0.3)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# ==================== 计算区间宽度统计 ====================
print('区间宽度统计:')
# 区间宽度 = 上限 - 下限
df_area['区间宽度'] = df_area['上限'] - df_area['下限']
# describe()计算区间宽度的统计量
print(df_area['区间宽度'].describe())金融应用:
- 置信区间:统计模型预测的置信区间
- 布林带:移动平均线 ± 标准差
- 期权策略:期权到期时的盈亏区间
- 风险管理:VaR(在险价值)的区间表示
fill_between参数:
alpha:透明度(0-1),越小越透明color:填充颜色where:条件填充,只填充满足条件的区域interpolate:在边界处插值,使填充更平滑
55.8 堆叠面积图
# =============================================================================
# 题目:堆叠面积图——投资组合构成
# =============================================================================
# 本任务演示如何绘制堆叠面积图,展示各部分随时间的变化及总体趋势
# 应用场景:投资组合资产配置、公司收入构成、市场份额变化
# ==================== 创建投资组合数据 ====================
np.random.seed(42) # 设置随机种子,确保每次运行结果相同
df_portfolio = pd.DataFrame({
'日期': pd.date_range('2024-01-01', periods=12), # 12个月
# 股票:100万元起,每月增长5万元,标准差3万元
'股票': [100 + i*5 + np.random.randn()*3 for i in range(12)],
# 债券:80万元起,每月增长2万元,标准差2万元
'债券': [80 + i*2 + np.random.randn()*2 for i in range(12)],
# 现金:20万元起,每月增长0.5万元,标准差1万元
'现金': [20 + i*0.5 + np.random.randn()*1 for i in range(12)]
})
# ==================== 绘制堆叠面积图 ====================
plt.figure(figsize=(12, 6))
# plt.stackplot()绘制堆叠面积图
# df_portfolio['日期']:X轴数据(时间)
# 接下来的三个参数是要堆叠的Y值序列
# df_portfolio['股票']:第一层(最底层)
# df_portfolio['债券']:第二层(堆叠在股票之上)
# df_portfolio['现金']:第三层(最顶层)
# alpha=0.8:透明度0.8(使区域不太透明)
# colors:颜色列表,分别对应三个序列
# labels:标签列表,用于图例
plt.stackplot(df_portfolio['日期'],
df_portfolio['股票'],
df_portfolio['债券'],
df_portfolio['现金'],
alpha=0.8,
colors=['#E3120B', '#008080', '#F0A700'],
labels=['股票', '债券', '现金'])
# ==================== 设置图表装饰 ====================
plt.title('投资组合资产配置变化', fontsize=16, fontweight='bold')
plt.xlabel('日期', fontsize=12)
plt.ylabel('资产价值(万元)', fontsize=12)
# loc='upper left':图例位置在左上角
plt.legend(loc='upper left', fontsize=11)
# axis='y':只显示Y轴网格线(水平网格线)
plt.grid(True, alpha=0.3, axis='y')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# ==================== 计算各资产占比 ====================
print('各资产占比:')
# total:每月的资产总值(股票+债券+现金)
# sum(axis=1):按行求和(横向求和)
total = df_portfolio[['股票', '债券', '现金']].sum(axis=1)
# 计算最新月份(最后一行)各资产占比
for asset in ['股票', '债券', '现金']:
# df_portfolio[asset].iloc[-1]:获取该资产的最后一行值(最新值)
# total.iloc[-1]:获取总资产的最后一行值
ratio = df_portfolio[asset].iloc[-1] / total.iloc[-1]
# f'{asset}: {ratio:.1%}':格式化输出,百分比保留1位小数
print(f'{asset}: {ratio:.1%}')堆叠图的数学含义:
\[ y_{\text{total}} = y_1 + y_2 + \cdots + y_n \]
每层的实际值:
\[ y_{\text{layer }i} = \sum_{j=1}^i y_j \]
金融应用:
- 资产配置:展示股票、债券、现金的配置变化
- 收入构成:主营业务收入、其他业务收入
- 市场份额:不同公司在市场中的份额变化
55.9 对数坐标轴
# =============================================================================
# 题目:对数Y轴——复利增长可视化
# =============================================================================
# 本任务演示如何使用对数坐标轴展示指数增长数据
# 应用场景:复利增长、投资回报、跨越数量级的数据
# ==================== 创建复利增长数据 ====================
# years:年份序列,从2000年到2023年
years = np.arange(2000, 2024)
initial_value = 100 # 初始值:100元
growth_rate = 0.15 # 年化增长率:15%
# values:使用复利公式计算每年的价值
# (1 + growth_rate) ** (years - 2000):复利因子
# 例如:2000年:(1.15)^0 = 1,价值100元
# 2001年:(1.15)^1 = 1.15,价值115元
# 2002年:(1.15)^2 = 1.3225,价值132.25元
values = initial_value * (1 + growth_rate) ** (years - 2000)
# ==================== 创建对比:线性vs对数 ====================
# plt.subplots(1, 2, figsize=(15, 6))创建1行2列的子图布局
# fig:图形对象
# (ax1, ax2):两个轴对象的元组
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
# ==================== 线性坐标(左图) ====================
# ax1.plot()在第一个子图绘制
ax1.plot(years, values, linewidth=2, color='#E3120B')
ax1.set_title('线性坐标', fontsize=14, fontweight='bold')
ax1.set_xlabel('年份', fontsize=12)
ax1.set_ylabel('价值(元)', fontsize=12)
ax1.grid(True, alpha=0.3)
# ==================== 对数坐标(右图) ====================
# ax2.semilogy()绘制Y轴为对数坐标的图表
# semilogy = semi-logarithmic y-axis(Y轴对数化)
# 在对数坐标下,指数增长呈直线
ax2.semilogy(years, values, linewidth=2, color='#008080')
ax2.set_title('对数坐标', fontsize=14, fontweight='bold')
ax2.set_xlabel('年份', fontsize=12)
# Y轴标签注明"对数",提醒读者
ax2.set_ylabel('价值(元,对数)', fontsize=12)
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# ==================== 输出复利增长分析 ====================
print('复利增长分析:')
print(f'初始值: {initial_value}元')
print(f'年化增长率: {growth_rate:.1%}')
print(f'2023年价值: {values[-1]:.2f}元')
# 计算总增长倍数
print(f'总增长倍数: {values[-1]/initial_value:.1f}倍')对数坐标的金融意义:
线性坐标:展示绝对值变化 对数坐标:展示相对变化(百分比)
在金融中:
- 复利效应:对数坐标下,恒定增长率呈直线
- 收益率比较:不同起始点的资产可以直接比较斜率
- 长期趋势:更适合展示跨越数量级的数据
数学原理:
\[ \ln(y) = \ln(A \cdot e^{rt}) = \ln(A) + rt \]
在半对数图上,指数增长呈直线,斜率即为增长率 \(r\)。
55.10 金融应用移动平均线
# =============================================================================
# 题目:移动平均线——技术分析基础
# =============================================================================
# 本任务演示如何绘制移动平均线,识别金叉和死叉信号
# 应用场景:技术分析、趋势跟踪、交易信号生成
# ==================== 创建价格数据 ====================
# np.random.seed(42)设置随机种子,确保每次运行结果相同
np.random.seed(42)
# df_price:模拟60个交易日的股价数据
df_price = pd.DataFrame({
'日期': pd.date_range('2024-01-01', periods=60),
# 收盘价:100元起
# np.random.normal(0.5, 2, 60)生成60个正态分布随机数(均值0.5,标准差2)
# np.cumsum()计算累积和,模拟价格的随机游走
'收盘价': 100 + np.cumsum(np.random.normal(0.5, 2, 60))
})
# ==================== 计算移动平均线 ====================
# .rolling(window=5).mean():5日移动平均线
# rolling(5):创建一个5日的滚动窗口
# mean():计算窗口内数据的均值
df_price['MA5'] = df_price['收盘价'].rolling(window=5).mean()
# 20日移动平均线(中期趋势线)
df_price['MA20'] = df_price['收盘价'].rolling(window=20).mean()
# 60日移动平均线(长期趋势线)
df_price['MA60'] = df_price['收盘价'].rolling(window=60).mean()
# ==================== 绘制移动平均线 ====================
plt.figure(figsize=(14, 7))
# 绘制收盘价(深灰色,半透明)
plt.plot(df_price['日期'], df_price['收盘价'],
label='收盘价', linewidth=1.5, color='#2C3E50', alpha=0.7)
# 绘制MA5(红色,短期均线)
plt.plot(df_price['日期'], df_price['MA5'],
label='MA5', linewidth=1.5, color='#E3120B')
# 绘制MA20(青色,中期均线)
plt.plot(df_price['日期'], df_price['MA20'],
label='MA20', linewidth=1.5, color='#008080')
# 绘制MA60(黄色,长期均线,线宽加粗)
plt.plot(df_price['日期'], df_price['MA60'],
label='MA60', linewidth=2, color='#F0A700')
# ==================== 标注金叉和死叉 ====================
# 金叉:短期均线上穿长期均线(买入信号)
# (MA5 > MA20):当前MA5大于MA20
# .shift(1):将数据下移一行,获取前一日的值
# (MA5.shift(1) <= MA20.shift(1)):前一日的MA5小于等于MA20
# 两个条件都满足时,表示MA5刚刚上穿MA20
golden_cross = (df_price['MA5'] > df_price['MA20']) & (df_price['MA5'].shift(1) <= df_price['MA20'].shift(1))
# 死叉:短期均线下穿长期均线(卖出信号)
death_cross = (df_price['MA5'] < df_price['MA20']) & (df_price['MA5'].shift(1) >= df_price['MA20'].shift(1))
# plt.scatter()绘制金叉点(红色向上三角)
# df_price['日期'][golden_cross]:金叉发生的日期
# df_price['MA5'][golden_cross]:金叉点的MA5值
# color='red':红色
# s=100:散点大小为100
# marker='^':向上三角符号
# label='金叉':图例标签
# zorder=5:图层顺序为5(确保散点显示在线条上方)
plt.scatter(df_price['日期'][golden_cross], df_price['MA5'][golden_cross],
color='red', s=100, marker='^', label='金叉', zorder=5)
# plt.scatter()绘制死叉点(绿色向下三角)
plt.scatter(df_price['日期'][death_cross], df_price['MA5'][death_cross],
color='green', s=100, marker='v', label='死叉', zorder=5)
# ==================== 设置图表装饰 ====================
plt.title('移动平均线与交叉信号', fontsize=16, fontweight='bold')
plt.xlabel('日期', fontsize=12)
plt.ylabel('价格(元)', fontsize=12)
plt.legend(loc='best', fontsize=11)
plt.grid(True, alpha=0.3)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# ==================== 输出移动平均线统计 ====================
print('移动平均线统计:')
print(df_price[['收盘价', 'MA5', 'MA20', 'MA60']].describe())移动平均线的金融理论:
简单移动平均(Simple Moving Average, SMA):
\[ \text{SMA}_t = \frac{1}{n}\sum_{i=0}^{n-1} P_{t-i} \]
交易信号:
- 金叉(Golden Cross):短期均线上穿长期均线,买入信号
- 死叉(Death Cross):短期均线下穿长期均线,卖出信号
局限性:
- 滞后性:基于历史价格,信号滞后
- 震荡市场:在横盘震荡中频繁产生错误信号
- 参数敏感性:不同周期效果差异大
55.11 本章小结
要点:
- 曲线图是函数 \(y=f(x)\) 的可视化:金融时间序列中以时间为自变量、价格为因变量,相邻点之间默认用线段相连
plt.plot(t, y, 'r--', label=..., lw=2.5)用格式串同时指定颜色与线型;label配合plt.legend(loc=0, fontsize=13)生成图例,loc=0表示自动选择最佳位置- 等额本息:月供固定,内部利息逐期递减、本金逐期递增,任意一期满足
ppmt + ipmt = pmt;等额本金:每月本金固定,利息按剩余本金逐月重算,月供总额逐期递减 numpy_financial按现金流方向取符号,还款额返回负值,绘图时常取负翻正(如-pay_month、-interest_pay_month)- 双Y轴用
ax2 = ax1.twinx()共享X轴,可同图展示价格与成交量等不同量纲的序列,但两个坐标轴的图例需用lines = line1 + line2手动合并 - 对数坐标(
semilogy)下恒定增长率的复利曲线呈直线,斜率对应增长率,适合展示跨越数量级的数据 fill_between填充上下限形成区间带(预测区间、布林带);stackplot堆叠展示构成,每层高度是前几层的累计值
易错点:
- 平台任务1第三条曲线的图例字符串是题面乱码(正确含义为”每月偿还利息金额”),平台按原始代码判定,本地自主练习时应写正确字符串
- 第18章的同一按揭任务用
ppmt(rate=r/12, per=n*12, ...)计算末期本金口径,本章用pmt计算月供口径,两章口径不同、各自正确,不要相互”改错” - 忘记对
pmt/ppmt/ipmt的返回值取负,画出的曲线会整体落在X轴下方 - 等额本金任务的利息需按剩余本金逐月循环计算,直接改用
ipmt就变成了等额本息口径 plt.savefig()应在plt.show()之前调用,否则本地保存出的是空白图片- 多条曲线只写
label而忘记调用plt.legend(),图例不会出现;fontsize参数多数时候属于legend/xticks等函数而非plot本身
55.12 动手与思考
以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。
自测回忆:不看正文,写出下列格式字符串各自对应的颜色与线型:
'r-'、'm--'、'b--'、'y--'、'c--';并回答lw、label、loc=0、fontsize分别应在哪个函数中传入。参考答案(点开前请先独立完成)
解题思路:格式串由”颜色字符 + 线型字符”拼成——
'r-'红色实线,'m--'品红色虚线,'b--'蓝色虚线,'y--'黄色虚线,'c--'青色虚线。参数归属:lw(线宽)与label(图例条目文本)在plt.plot()等绘图函数中传入;loc=0在plt.legend()中传入(0 表示自动选择最佳位置);fontsize多数时候属于plt.legend()、plt.xticks()、plt.yticks()等标注类函数,而非plot本身。回扣主线:格式串与图例参数的归属见第 章节 18 章与本章”要点”第 2 条。
输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。
import numpy as np t = np.arange(6) + 1 pay = np.ones_like(t) * 30000.0 print(t) print(pay) print(len(np.linspace(0.02, 0.08, 100)))参考答案(先写下你的预测再点开)
解题思路:
np.arange(6)生成 0 至 5,加 1 后为 1 至 6(期数编号);np.ones_like(t)生成与t同形状的全 1 整数数组,乘 30000.0 后变为浮点型 30000.0(月供为常数序列);np.linspace(0.02, 0.08, 100)在 2% 至 8% 之间取 100 个等差点,长度为 100。# 验证脚本:期数序列、常数月供序列与利率网格长度 import numpy as np # 导入NumPy库 t = np.arange(6) + 1 # 1至6的期数编号 pay = np.ones_like(t) * 30000.0 # 同形状全1数组乘常数得等额月供序列 print(t) # 输出期数 print(pay) # 输出月供序列 print(len(np.linspace(0.02, 0.08, 100))) # 利率网格的点数预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
[1 2 3 4 5 6] [30000. 30000. 30000. 30000. 30000. 30000.] 100回扣主线:
arange/linspace/ones_like的行为见第 章节 5 章(经本章平台任务复用)。思考题:等额本息与等额本金两种规则下,“每月还款总额”曲线一条水平、一条递减。请从”每月偿还本金是否固定、利息按什么基数计算”出发解释这一差异,并说明为什么两条”每月偿还利息”曲线都是递减的。
参考答案(点开前请先独立完成)
解题思路:等额本息的定义是”每月还款总额固定”,把每月月供拆开看,其中本金部分逐月递增、利息部分逐月递减、两者之和恒定——利率息按剩余本金计提,随还款推进剩余本金下降,利息逐月变少,月供中腾出的份额自动补到本金上。等额本金是”每月偿还本金固定”,利息同样按剩余本金计提,剩余本金线性下降使利息近似线性递减,于是月供总额(固定本金 + 递减利息)逐月递减。两条利息曲线都递减的共同根源:利息恒等于”剩余本金 × 月利率”,而两种规则下剩余本金都单调下降,只是下降路径不同——等额本金线性下降,等额本息先慢后快。
回扣主线:两种还款规则的口径对照见第 章节 18 章同一按揭任务与本章”要点”第 3 条。
变式任务(平台任务同型改造):仿照平台任务2的利率敏感性分析,把横轴从贷款利率改为贷款期限——本金100万元、年利率4.9%、期限从5年到30年取整数年,绘制等额本息月供随期限变化的曲线,并用一个圆点标出30年期的月供;观察曲线下降速度为什么越来越慢(凹向如何)。
参考答案(点开前请先独立完成)
解题思路:用
npf.pmt逐个期限计算月供并取负翻正。实测:5 年期月供 18825.45 元,30 年期 5307.27 元;期限从 5 年延到 10 年月供减少 8267.71 元,而从 25 年延到 30 年只减少 480.52 元——下降速度越来越慢,曲线递减且下凸(凹函数,二阶导大于 0)。原因:期限拉长把固定本金摊到更多期,每期本金份额的边际缩减随期数增多而变薄,而利息部分趋近一个稳定占比,月供随期限趋于一个渐近下限而不是降到零。# 变式脚本:月供对期限的敏感性曲线 import matplotlib.pyplot as plt # 导入绘图库 import numpy as np # 导入NumPy库 import numpy_financial as npf # 导入金融计算库 rate_annual = 0.049 # 年利率4.9% principal = 1000000.0 # 贷款本金100万元 terms = np.arange(5, 31) # 期限5至30年取整数年 pay_by_term = -npf.pmt(rate_annual / 12, terms * 12, principal) # 各期限月供(取负翻正) print(round(pay_by_term[0], 2), round(pay_by_term[-1], 2)) # 5年期与30年期月供 print(round(pay_by_term[0] - pay_by_term[5], 2), round(pay_by_term[20] - pay_by_term[25], 2)) # 两段边际降幅对照 plt.plot(terms, pay_by_term / 10000, 'b--', lw=2, label='等额本息月供') # 月供折算万元后画曲线 plt.scatter([30], [pay_by_term[-1] / 10000], color='red', s=80, zorder=5, label='30年期月供') # 圆点标注30年期 plt.xlabel('贷款期限(年)') # X轴标签 plt.ylabel('月供(万元)') # Y轴标签 plt.legend() # 显示图例 plt.savefig('pay_by_term.png') # 保存图形预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
18825.45 5307.27 8267.71 480.52图形:曲线自左上(5 年期约 1.88 万元/月)向右下单调递减,前段陡峭、后段平缓,红色圆点落在曲线右端(30 年期约 0.53 万元/月),整体下凸。
注意:以上为变式代码;列表 55.2 的平台任务仍须按原始代码原样输入教学平台。
回扣主线:利率敏感性分析的结构与
pmt取负翻正的口径见本章平台任务 2 解析与第 章节 18 章按揭例。变式任务:把”金融应用:移动平均线”一节的双均线从MA5与MA20改为MA10与MA60,重新统计金叉、死叉的个数与位置,并讨论窗口变长后信号数量与滞后性如何变化。
参考答案(点开前请先独立完成)
解题思路:先看数据约束——该节的模拟样本只有 60 个交易日,
rolling(60)只在最后 1 个位置窗口才装满,MA60 全程只有 1 个有效值;而交叉的判定要求相邻两天两条均线同时有效,因此 MA10/MA60 在 60 日样本上金叉、死叉均为 0 个(对照:同一样本的 MA5/MA20 有 2 个金叉,位于索引 33、55,1 个死叉,位于索引 39)。把样本延长到 120 日(同一随机种子,路径是 60 日版本的延长)后再统计:MA5/MA20 增至 3 个金叉(索引 33、55、107)与 2 个死叉(索引 39、103),MA10/MA60 仍为 0——本模拟路径带上升趋势,MA10 自 MA60 首个有效期起一直位于其上方,从未回落穿越。结论:窗口越长,均线越平滑、对短期波动的响应越迟钝,交叉信号数量通常越少、滞后越强,甚至在单边趋势样本中整段无信号;同时长窗口要求更长的事前数据,否则起始段大面积为 NaN。# 变式脚本:两组均线窗口的交叉统计对照 import pandas as pd # 导入pandas库 import numpy as np # 导入NumPy库 for n_days in (60, 120): # 原样本60日与其延长版120日 np.random.seed(42) # 与正文同一随机种子,路径前段一致 close_s = pd.Series(100 + np.cumsum(np.random.normal(0.5, 2, n_days))) # 模拟收盘价 ma5, ma20 = close_s.rolling(5).mean(), close_s.rolling(20).mean() # 短中期均线 ma10, ma60 = close_s.rolling(10).mean(), close_s.rolling(60).mean() # 变式均线组 up52 = (ma5 > ma20) & (ma5.shift(1) <= ma20.shift(1)) # MA5上穿MA20 down52 = (ma5 < ma20) & (ma5.shift(1) >= ma20.shift(1)) # MA5下穿MA20 up1060 = (ma10 > ma60) & (ma10.shift(1) <= ma60.shift(1)) # MA10上穿MA60 down1060 = (ma10 < ma60) & (ma10.shift(1) >= ma60.shift(1)) # MA10下穿MA60 print(n_days, 'MA5/MA20金叉', np.flatnonzero(up52.to_numpy()).tolist(), '死叉', np.flatnonzero(down52.to_numpy()).tolist()) # 输出交叉位置 print(n_days, 'MA10/MA60金叉', np.flatnonzero(up1060.to_numpy()).tolist(), '死叉', np.flatnonzero(down1060.to_numpy()).tolist()) # 输出交叉位置预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
60 MA5/MA20金叉 [33, 55] 死叉 [39] 60 MA10/MA60金叉 [] 死叉 [] 120 MA5/MA20金叉 [33, 55, 107] 死叉 [39, 103] 120 MA10/MA60金叉 [] 死叉 []